package compilers.stmt;

import java.io.BufferedWriter;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Vector;

import org.antlr.runtime.Token;

import compilers.Node;
import compilers.condition.Param;
import compilers.condition.Return;
import compilers.node.Identifier;
import compilers.node.Stmt;
import compilers.output.ErrorOutput;
import compilers.output.WarningOutput;
import compilers.types.Type;
import compilers.types.VoidType;
import compilers.table.MemScope;

public class FunctionType extends Stmt {
	Identifier fName;
	Type fReturnType;
	Stmt fStatement;
	Vector<Param> fParameters = new Vector<Param>();
	boolean hasReturn = false;
	boolean hasBody = false;
	
	public FunctionType(Type fReturnType, Identifier fName) {
		super();
		this.setfName(fName);
		this.fReturnType = fReturnType;
	}
	
	public void setBody() {
		hasBody = true;
	}
	
	public void addStatement (Stmt fStatement) {
		this.fStatement = fStatement;
	}

	public FunctionType(Type returnType) {
		this.fReturnType = returnType;
	}
		
	public void addParameter(Param fParam) {
		getfParameters().add(fParam);
	}
	
	public boolean body() {
		if (hasBody) {
			return hasBody;
		}
		return fStatement!=null;
	}
	
	public boolean hasReturn() {
		return hasReturn;
	}

	public Type getReturnType() {
		return fReturnType;
	}

	public boolean isNumeric() {
		return false;
	}
	
	public int parameterCount() {
		return getfParameters().size();
	}
	
	public void checkReturnType(Type type, Return ret, Token token) {
		ret.setTypes(type, fReturnType);
		hasReturn = true;
		if (type == null) {
			type = new VoidType(true);
		}
		
		if(fReturnType.getClass() == VoidType.class && type.getClass() != VoidType.class) {
			new ErrorOutput(token, "Return type is void");
		}
		
		if (!fReturnType.getClass().isInstance(type)) {
			new WarningOutput(token, "Return type with possible data loss.");
		}
	}
	
	public void checkParameters(Vector<Param> parameters, Token token, boolean call) {
		if (parameters.size() != getfParameters().size()) {
			new ErrorOutput(token, "No function found for the call");
		}
		for (int i = 0; i < parameters.size(); i++) {
			if	(call) {
				if (!getfParameters().get(i).getType().getClass().isInstance(parameters.get(i).getType())) {
					if(parameters.get(i).getType() == null) continue;
					new WarningOutput(token, "Parameter types assigned but with possible data loss.");
				}
				if ((parameters.get(i).getType() != null) && (getfParameters().get(i).getType() != null)) {
					if ((parameters.get(i).getType().getfIdentifier() != null) && (getfParameters().get(i).getType().getfIdentifier() != null)) { 
						if (!parameters.get(i).getIdentifier().getIndices(token).isEmpty() && !getfParameters().get(i).getIdentifier().getIndices(token).isEmpty()) {
							new ErrorOutput(token, "Types in parameter list do not match (array vs non-array).");
						}
					}
				}
			} else {
				if (getfParameters().get(i).getType().getClass() != parameters.get(i).getType().getClass()) {
					new ErrorOutput(token, "Types in parameter list do not match.");
				}
				if ((getfParameters().get(i).getIdentifier() != null) && (parameters.get(i).getIdentifier() != null)) {
					if  (getfParameters().get(i).getIdentifier().getString().compareTo(parameters.get(i).getIdentifier().getString()) > 0) {
						new ErrorOutput(token, "Name in parameter list does not match: " + getfParameters().get(i).getIdentifier().getString() + " and " + parameters.get(i).getIdentifier().getString());
					}
				}
			}
		}
	}

	@Override
	public void print(int tab) {
		for (int i=1;i<=tab;i++){System.out.print("\t");}System.out.print("|-");
		System.out.println("Function");
		fReturnType.print(tab+1);
		getfName().print(tab+1);
		Enumeration<Param> enumeration = getfParameters().elements();
		while(enumeration.hasMoreElements()){
			((Node) enumeration.nextElement()).print(tab+1);
		}  
		if (fStatement != null)
			fStatement.print(tab+1);
	}
	
	public void arrayIndices(int index, MemScope scope, Vector<Integer> set, String name) {
		Vector<Integer> ind = scope.getArray(name).getIndices(null);
		if (index < ind.size()) {
			for (int i = 0; i < ind.get(index).intValue(); i++) {
				set.set(index, i);
				arrayIndices((index+1), scope, set, name);
			}
		} else {
			//String orig = name;
			char c[] = name.toCharArray();
			for (int k = 0; k < c.length; k++) {
				if (c[k] == '[') {
					name = name.substring(0, k);
					break;
				}
			}
			for (int i = 0; i < set.size(); i++) {
				name += "[" + set.get(i).intValue() + "]";
			}
			scope.addParam(name);
		}
	}

	public void generate(BufferedWriter writer, MemScope scope) throws IOException {
		if (fStatement == null) {
			// decl
			scope.addFunction(fName.getfIdentifier());
		} else {
			int ssp = 5;
			MemScope myScope = new MemScope(scope, fName.getfIdentifier());
			
			for (int i = 0; i < getfParameters().size(); i++) {
				String name = getfParameters().get(i).getIdentifier().getfIdentifier();
				if (myScope.isArray(name)) {
					Vector<Integer> set = new Vector<Integer>();
					Vector<Integer> ind = myScope.getArray(name).getIndices(null);
					for (int j = 0; j < ind.size(); j++) {
						ssp += ind.get(i).intValue();
						set.add(0);
					}
					arrayIndices(0, myScope, set, name);
				} else {
					ssp++;
					myScope.addParam(getfParameters().get(i).getIdentifier().getfIdentifier());
				}
			}

			if (scope != null) {
				if (scope.getfParentScope() != null) {
					// nested function, jump to end
					writer.write("ujp " + myScope.getName() + "_end\n");
				}				
			}
			scope.addFunction(fName.getfIdentifier());				
			
			myScope.setStart(ssp);
			
			writer.write(myScope.getName() + ":\n");
			writer.write("ssp " + (ssp + fStatement.getfVarCount()) + "\n");	
		
			fStatement.setfResReturn(fReturnType);
			fStatement.generate(writer, myScope);
			
			if (fReturnType instanceof VoidType) {
				writer.write("retp\n");
			} 
			writer.write(myScope.getName() + "_end:\n");	
		}
	}

	public Identifier getfName() {
		return fName;
	}

	public void setfName(Identifier fName) {
		this.fName = fName;
	}

	public Vector<Param> getfParameters() {
		return fParameters;
	}

	public void setfParameters(Vector<Param> fParameters) {
		this.fParameters = fParameters;
	}
		
}
